import utility
import cPickle, StringIO, pickle
import os, scipy, numpy
from collections import deque
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.Util import number
from random import randrange

class HistoryFile:
    """A Class implementing a History File."""

    """
    - it handles plain and ciphertext HFs, low-level calculating (calculating hpwd) is done by CryptoSystem    
    - encrypting, decrypting with hpwd - does not need to store hpwd
    - serialize, deserialize history file is the full transaction of read(write) and decrypt(encrypt)
    - update to replace the history file with new one
    """

    """
    Initializes the History File.
    Parameters:
    feature_vector_size: size of the feature vector.
    history_length: the length of the log (feature vectors are stored for this number of succesful logins).
    Note: desired padding size is computed from these two values.
    
    path: this is where the History File is stored.

    Returns: None.
    """
    def __init__(self, feature_vector_size, history_length, path, hpwd = None):

        #size, length must be valid integers.
        try:
            if (abs(int(feature_vector_size)) < 1):
                utility.debug('Invalid feature vector size.')
                return None
            if (abs(int(history_length)) < 1):
                utility.debug('Invalid history length.')
                return None
            self.feature_vector_size = abs(int(feature_vector_size))
            self.size = abs(int(feature_vector_size*history_length))
            self.history_length = abs(int(history_length))
        #ValueError, TypeError, AttributeError
        except:
            utility.debug('Argument size is not an integer.')
            return None
            
        self.path = os.path.join(path, "history.dat")

        try:
            hpwd = long(hpwd)
        #ValueError, TypeError, AttributeError
        except:
            utility.debug('Argument hpwd is invalid.')
            return None        
        
        self.cryptObj = AES.new((SHA256.new(number.long_to_bytes(hpwd))).digest())
        if os.path.exists(self.path):
            self.serialized = True
            if (self.deserialize() == False):
                utility.debug('Deserialization Failed. Abort History File.')
                return None
        else:
            Q = deque()
            for i in range(history_length):                
                Q.append([2 for i in range(feature_vector_size)])  # 20 = MAX_PWD
            self.contents = Q

        #indicates whether the History File is currently serialized or deserialized.
        self.serialized = False

    #Private functions, called by serialize/deserialize
    """
    Encrypts the history file (stored in self.content) using a symmetric cipher, using hpwd. The encrypted file will be in self.content.

    Parameters:
    content: the raw content to encrypt (should be padded by now)

    Output: True if encryption is successful, False if it failed.
    """
    def _encrypt(self, content):
        try:
            self.contents = self.cryptObj.encrypt(content)
        except:
            utility.debug('Encryption failed.')
            return False
        return True

    """
    Decrypts the history file (in self.content) using a symmetric cipher, using hpwd. The encrypted file will be in self.content.

    Parameters:
    content: the content to decrypt

    Output: True if decryption is successful, False if it failed.
    """
    def _decrypt(self, content):
        # try / except around here

        try:
            self.contents = self.cryptObj.decrypt(content)            
        # Python v2.6 only
        #except ValueError as v:
        #    utility.debug('System Error Message: %s' % v)
        except:
            utility.debug('Decryption failed.')
            return False
        return True

    """
    Pads the decrypted history_file

    Parameters:
    pad_length: the length of the pad to be added.

    Output: True if padding is successful, False if it failed.
    """    
    def _pad(self, pad_length):
        pb = len(self.contents) % pad_length
        for i in range(pad_length - pb): 
            self.contents += chr(randrange(0, 256))
        return True

    #Public functions
    """
    Returns the accepted feature_vector based on history.
    Should be called on deserialized HistoryFile.

    Parameters: None

    Output: feature_vector: the feature vector for the users password.
    f[i] = -1, if lower_case only for length h in history file, 1 if upper_case only, 0 if either
    pol: the random polynomial used
    """
    def get_feature_vector(self):
        if self.serialized:
            utility.debug('Not deserialized.')
            return False
        else:
            feature_vector = []
            for i in range(self.feature_vector_size):
                try:
                    #2 is a placeholder (joker) for the initial setup. as long as its in there, the history length has not been reached > accept everything
                    if (2 in [l[i] for l in self.contents]):
                        feature_vector = [0 for i in range(self.feature_vector_size)]
                        return feature_vector
                    else:                    
                        if 0 in [l[i] for l in self.contents]:
                            if 1 in [l[i] for l in self.contents]:
                                feature_vector.append(0)
                            else:
                                feature_vector.append(-1)
                        else:                    
                            feature_vector.append(1)
                except:
                    utility.debug('Feature vector update failed')
                    return False
        return feature_vector

    """
    Updates the history file (deserialized into an in-memory structure) with a new vector of feature_values (vector of capital/lower case values).
    SecLogin first calls deserialize once it got hpwd. Then Calls update to put in the new values. Then it calls serialize to encrypt and write back HF.

    Parameters:
    feature_values: vector of 1 bit information (capital or lowercase)

    Returns: True if update was successful, False otherwise.
    """    
    def update(self, feature_values):        
        #check feature_values size against feature_vector size
        try:
            if (len(feature_values) != self.feature_vector_size):
                utility.debug('Invalid argument.')
                return False
            #check that feature_values are all 0s or 1s
            for i in feature_values:
                if i not in [0,1]:
                    utility.debug('Invalid argument.')
                    return False
        #ValueError, TypeError, AttributeError
        except:
            utility.debug('Invalid argument.')
            return False          
        if self.serialized:
           return False        
        try:
            self.contents.popleft()
            self.contents.append(feature_values)
        # Python v2.6 only
        #except ValueError as e:
        except:
            utility.debug('Update failed.')
            return False
        return True

    """
    Using the supplied hpwd, it reads the encrypted History File from disk, decrypts is and deserializes it into a in-memory History File data structure.

    Parameters:
    content: The content to deserializer using the cPickly module.

    Returns: True if deserializtion is successful, False otherwise.
    """
    
    def deserialize(self):
        if (not self.serialized):
            utility.debug('The contents are already deserialized.')
            return True
        try:
            fh = open(self.path, 'r')
        except:
            utility.debug('File opening failed.')
            return False
        if (self._decrypt(fh.read()) == False):
            utility.debug('Decryption failed, aborting.')
            fh.close()
            return False
        try:
            #self.contents = cPickle.loads(self.contents)
            everything = pickle.loads(self.contents)
            content = everything[0]
            hash_value = everything[1]
            content_hashed = SHA256.new(str(content)).digest()

            if (hash_value == content_hashed):
                utility.debug('Signature correct.')
            else:
                utility.debug('Signature incorrect, abort.')
                return False            
            self.contents = content

            #FIXME: this does not mean yet that self.contents is a proper array of arrays of 0,1 numbers!!!! It might be something bad. check this.
        
        # Python v2.6
        #except cPickle.UnpicklingError as u:
        #    utility.debug('Unpickling error deserializing: %s' % u)
        except:
            utility.debug('History File verification failed.')
            fh.close()
            return False
        
        fh.close()
        self.serialized = False
        return True

    """ 
    Using the supplied hpwd, it serializes History File to a bytestream, encrypts it (with redundancy) and writes it to disk.

    Parameters:
    content: The content to parse and dump into buff.

    Returns: True if serialization is successful, False otherwise.
    """
    def serialize(self):
        try:
            buf = StringIO.StringIO()
            pObj = cPickle.Pickler(buf)
            # Calculating the hash value of our feature vector collections object & storing that in a tuple 
            hashValue = SHA256.new(str(self.contents)).digest()
            everything = (self.contents, hashValue)
            # Dumping the contents to a pickled object
            pObj.dump(everything)
            # Storing everything serialized in memory.
            self.contents = buf.getvalue()
            buf.close()
           
            self._pad(self.cryptObj.block_size)   # Should be 16 for AES instantiation, by default
            if (self._encrypt(self.contents) == False):
                utility.debug('Serialization failed during encryption.')
                return False
            
            fh = open(self.path, 'w')
            fh.write(self.contents)
            fh.close()
            self.serialized = True
            return True
        except:
            utility.debug('Serialization error, abort.')
            return False

#Insert unit test here for verifying module implementation
if __name__ == "__main__":
    print 'Unit Test'
    obj = HistoryFile(11, 10, "/tmp/", 0)
    try:
        assert(obj != None)
        assert(obj.update([1,0,0,1,0,1,0,0,1,0,1]) != False)
        assert(obj.update([1,0,0,1,0,1,0,0,1,0,1]) != False)

        assert(obj.serialize() != False)
        assert(obj.deserialize() != False)
    except AssertionError:
        utility.debug('Unit Test failed.')
    utility.debug('Unit test successful.')
    
